package com.vlefen.springcloud.grpc.grpc;

import com.google.common.base.Preconditions;
import com.google.common.net.InetAddresses;
import com.vlefen.springcloud.grpc.config.GRpcProperties;
import io.grpc.Server;
import io.grpc.ServerServiceDefinition;
import io.grpc.health.v1.HealthCheckResponse;
import io.grpc.netty.NettyServerBuilder;
import io.grpc.services.HealthStatusManager;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.util.NetUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ResourceUtils;
import org.springframework.util.StringUtils;

import javax.net.ssl.SSLException;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.SocketAddress;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;

@Slf4j
public class GRpcServer implements RpcServer {


    private final HealthStatusManager healthStatusManager = new HealthStatusManager();
    private final GRpcProperties gRpcProperties;
    private final AtomicInteger serverCounter = new AtomicInteger(0);
    private final Set<ServerServiceDefinition> services;
    private Server server;

    public GRpcServer(Set<ServerServiceDefinition> services, GRpcProperties gRpcProperties) {
        this.gRpcProperties = gRpcProperties;
        this.services = services;
    }

    /**
     * @return
     */
    public SslContextBuilder sslContextBuilder() {
        return null;
    }

    public void start() {

        if (CollectionUtils.isEmpty(services)) {
            log.warn("can't find grpc service, not need to start grpc server ");
            return;
        }
        String address = gRpcProperties.getServer().getAddress();
        int port = gRpcProperties.getServer().getPort();
        if (StringUtils.isEmpty(address)) {
            address = "127.0.0.1";
        }


        NettyServerBuilder serverBuilder = null;
        SocketAddress socketAddress = null;
        try {
            port = createServerSocket(port, address);
            gRpcProperties.getServer().setPort(port);
            socketAddress = new InetSocketAddress(InetAddresses.forString(address), port);
        } catch (IOException e) {
            throw new RuntimeException("start rpc server failed", e);
        }
        serverBuilder = NettyServerBuilder.forAddress(socketAddress);
        serverBuilder.addService(healthStatusManager.getHealthService());

        for (ServerServiceDefinition service : services) {
            serverBuilder.addService(service);
            healthStatusManager.setStatus(service.getServiceDescriptor().getName(), HealthCheckResponse.ServingStatus.SERVING);
            log.info("add service:{}", service.getServiceDescriptor().getName());
        }


        GRpcProperties.ServerSsl ssl = gRpcProperties.getServer().getSsl();
        SslContextBuilder sslContextBuilder = sslContextBuilder();
        if (ssl.isEnabled() && sslContextBuilder != null) {
            try {
                serverBuilder.sslContext(sslContextBuilder.build());
            } catch (SSLException e) {
                throw new IllegalArgumentException("configuration ssl failed");
            }
        }
        if (ssl.isEnabled() && sslContextBuilder == null) {

            String keyCertChain = ssl.getKeyCertChainFile();
            String key = ssl.getKeyFile();
            Preconditions.checkNotNull(keyCertChain, "not configuration keyCertChainFile for ssl ");
            Preconditions.checkNotNull(key, "not configuration keyFile for ssl ");
            try {
                File keyCertChainFile = ResourceUtils.getFile(keyCertChain);
                File keyFile = ResourceUtils.getFile(key);
                sslContextBuilder = SslContextBuilder.forServer(keyCertChainFile, keyFile, ssl.getKeyPassword());
                if (Objects.nonNull(ssl.getClientAuth())) {
                    sslContextBuilder.clientAuth(ssl.getClientAuth());
                }

                try {
                    serverBuilder.sslContext(sslContextBuilder.build());
                } catch (SSLException e) {
                    throw new IllegalArgumentException("configuration ssl failed");
                }
            } catch (FileNotFoundException e) {
                log.error("server ssl keyCertChainFile or keyFile file({},{}) not exists!", keyCertChain, key);
                throw new IllegalArgumentException("server ssl keyCertChainFile or keyFile  file not exists!");
            }
        }

        server = serverBuilder.build();

        try {
            server.start();
        } catch (IOException e) {
            log.error("start GRpc server({}:{}) failed", gRpcProperties.getServer().getAddress(), gRpcProperties.getServer().getPort());
            throw new RuntimeException("start GRpc server failed:", e);
        }
        log.info("start GRpc server({}:{})", gRpcProperties.getServer().getAddress(), server.getPort());
        Thread awaitThread = new Thread(
                "container-" + (serverCounter.incrementAndGet())) {

            @Override
            public void run() {
                try {
                    server.awaitTermination();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }

        };
        awaitThread.setDaemon(false);
        awaitThread.start();
    }


    public void stop() {
        if (null != server) {
            server.shutdown();
        }
    }

    @Override
    public boolean running() {
        return null == server ? false : server.isShutdown();
    }

    private static int createServerSocket(int port, String address) throws IOException {
        if (port < 0 || port > 0xFFFF) {
            port = 0;
        }

        for (int i = 0; i < 0xFFFF - port; i++) {
            try {
                ServerSocket serverSocket = new ServerSocket(port + i, 1, InetAddresses.forString(address));
                int localPort = serverSocket.getLocalPort();
                serverSocket.close();
                return localPort;
            } catch (IOException e) {
                log.warn("");
            }
        }
        throw new IOException("can't find available port");
    }
}
